const fs = require('fs');
const stream = require('stream');
const zlib = require('zlib');
const path = require('path');
const util = require('util');
const { console_debug } = require('./debug.js')

const {
  DB_DIR, 
  INDEX_SIZE, 
  OFFSET_SIZE,
  CONTENT_SIZE
} = require('./config.js');

const fs_access = util.promisify(fs.access);
const fs_open = util.promisify(fs.open);
const fs_read = util.promisify(fs.read);
const fs_write = util.promisify(fs.write);

// filter for \u0000 or \0
String.prototype.trimEmptyChar = function () {
  return this.split('\0')[0];
}

class IcyPoorDB {

  /**
   * use stream to load `index`
   * use fs.read to load one piece of data according to index
   * 
   * @typedef index
   * @property {string(offset)} [string(token)]
   * 
   * @param {string} name database name
   * @param {index} memory_index
   */
  constructor(name) {
    this.name = name;

    this.memory = {};
    // this.fragment = [];
    this.next_index = 0;

    this.INDEX_PATH = `${DB_DIR}/${name}.index`;
    this.STORE_PATH = `${DB_DIR}/${name}.store`;
    // TODO: record the fragmentation in database file and make full use of it
    // this.FRAGMENT_PATH = `${DB_DIR}/${name}.fragment`;

    // TODO: use Event Loop to limit multiply `read` or `write`
    this._load_index(name);
  }

  /********** INIT **********/
  _load_index(name) {

    this._init_db(name)

      .then(() => fs_open(this.INDEX_PATH, 'r'))

      .then(fd => {
        console_debug(`[OPEN_R_${fd}]: ${this.INDEX_PATH} is opened.`);
        this._read_one_index(fd);
      });

  }
  
  _init_db(name) {

    return Promise.all([
      this._ensure_file_existed(this.INDEX_PATH),
      this._ensure_file_existed(this.STORE_PATH)
    ])
    .then(([path1, path2]) => Promise.resolve(this.INDEX_PATH));
  }

  async _ensure_file_existed(file_path) {
    
    return fs_access(file_path)
      
      .then(() => {
        console_debug(`[INFO]: ${file_path} existed.\r\n`);
        return file_path;
      })
      
      .catch(err => {
        console_debug(`[DO]: Creating ${file_path}...\r\n`);
        fs.createWriteStream(file_path, '');
        return file_path;
      });

  }

  /**
   * 
   * @param {number} fd 
   */
  _read_one_index(fd) {
    
    // TODO: optimize this buffer ?
    let index_buf = Buffer.alloc(INDEX_SIZE),
        offset_buf = Buffer.alloc(OFFSET_SIZE),
        index_pos = this.next_index * (INDEX_SIZE + OFFSET_SIZE),
        offset_pos = index_pos + INDEX_SIZE,
        file_handle = 0,
        index = '';

    this.next_index++;

    // TODO: exception handler
    fs_read(fd, index_buf, 0, INDEX_SIZE, index_pos)
      
      .then(({bytesRead, buffer}) => {
        
        if (bytesRead === 0) {
          return null;
        }
        
        index = buffer.toString().trimEmptyChar();
        file_handle = fd;
        
        return fs_read(file_handle, offset_buf, 0, OFFSET_SIZE, offset_pos);
      })
      
      .then((value) => {
        
        if (!value) {
          fs.close(file_handle, err => true);
          console_debug(`[FINISH_LOAD]: ${JSON.stringify(this.memory)}`);
          return null;
        }

        // write key and value into memory index
        this.memory[index] = Number(value.buffer.toString().trimEmptyChar());
        this._read_one_index(file_handle);

      });
    
  }

  /********** READ **********/
  async read(index) {
    const read_buf = Buffer.alloc(CONTENT_SIZE);

    let file_handle = 0,
        content_offset = 0;

    return this._get_content_offset(index)

      .then((offset) => {

        if (typeof offset === 'undefined') {
          return null;
        }

        content_offset = offset;
        return fs_open(this.STORE_PATH, 'r');
      })

      .then(fd => {
        file_handle = fd;
        return fs_read(file_handle, read_buf, 0, CONTENT_SIZE, content_offset)
      })
      
      .then(({bytesRead, buffer}) => {
        return Promise.resolve(buffer.toString().trimEmptyChar());
      });
  }

  async _get_content_offset(index) {
    return this.memory[index];
  }

  /********** WRITE **********/
  async add(index, data) {
    
    await this._add_index(index);
    await this._add_content(data);
    
    // pointer move to next point where the next index will be written
    this.next_index++;
    return true;
  }

  _add_index_to_memory(key_name, offset) {
    this.memory[key_name] = offset;
  }

  _add_index(key_name) {
    const write_buf = Buffer.alloc(INDEX_SIZE + OFFSET_SIZE);
    const next_index_pos = this.next_index * (INDEX_SIZE + OFFSET_SIZE);
    const next_content_pos = this.next_index * CONTENT_SIZE;

    write_buf.write(key_name);
    write_buf.write(String(next_content_pos), INDEX_SIZE, OFFSET_SIZE, 'utf8');

    this._add_index_to_memory(key_name, next_content_pos);

    this._write_data(this.INDEX_PATH, write_buf, 0, INDEX_SIZE + OFFSET_SIZE, next_content_pos);
  }

  _add_content(content) {
    const write_buf = Buffer.alloc(CONTENT_SIZE);
    const next_content_pos = this.next_index * CONTENT_SIZE;

    write_buf.write(content);

    this._write_data(this.STORE_PATH, write_buf, 0, CONTENT_SIZE, next_content_pos);
  }

  _write_data(file_path, write_buf, buf_start, buf_len, write_position) {
    let file_handle = 0;

    fs_open(file_path, 'w')
      
      .then(fd => {
        console_debug(`[OPEN_W_${fd}]: ${file_path} is opened.`)
        file_handle = fd;
        return fs_write(file_handle, write_buf, buf_start, buf_len, write_position);
      })

      .then(({bytesWritten, buffer}) => {
        console_debug(`[WRITE]: ${bytesWritten}\t[Buffer]: ${buffer}`);
        fs.close(file_handle, err => true);
      });
  }


  /********** DELETE **********/
  async delete() {

  }

  /********** MODIFY **********/
  async modify(index, new_data) {

  }
}

module.exports = IcyPoorDB;